// template/data caching support
define(
  ["require",
   "jquery",
   "bip.util",
   "zlib",
  ],
  function(require) {

    var $ = require("jquery");
    var $cacheList = require("bip.util");
    var z = require("zlib");
    var lruDefaultOpts = {
        cacheMB : 200,
        maxLivingMinutes : 480,
        compressMinKB : 5,
    }; 
    function LRUCache(options) {
        this._opts = $.extend({}, lruDefaultOpts, options);
        this.list = new $cacheList.LinkedList();
        this.cachedBytes = 0;
        this.cachedData = {};
    }

    LRUCache.prototype.get = function(key) {
        cacheKey = toCacheKey(key);
        var node = this.cachedData[cacheKey];
        if (node != undefined) {
            if (!isExpired(node, this._opts)) {
                this.list.moveToFirst(node);
                if (node.v.stringData) {
                    return node.v.compressed ? uncompress(node.v.data) : node.v.data;
                }
                return JSON.parse(node.v.compressed ? uncompress(node.v.data) : node.v.data);
            }
            else {
                this.remove(key);
            }
        }
        return;
    }
    
    LRUCache.prototype.set = function(key, val) {
        var that = this;
        cacheKey = toCacheKey(key);
        function cacheItem(key, val) {
            this.key = key;
            this.refreshAt = new Date().getTime();
            this.stringData = typeof (val) == "string";
            var strVal = this.stringData ? val : JSON.stringify(val);
            this.compressed = (this.key.length + strVal.length) * 2 > that._opts.compressMinKB * 1024;
            this.data = this.compressed ? compress(strVal) : strVal;
            var valBytes = this.compressed ? this.data.length : this.data.length * 2;         
            this.itemBytes = this.key.length * 2 + valBytes;
        }

        var item = new cacheItem(cacheKey, val);
        var node = this.cachedData[cacheKey];
        var maxCacheBytes = MBToBytes(this._opts.cacheMB);
        
        if (item.itemBytes > maxCacheBytes) {
            if (node != undefined) { this.remove(key); }
            return;
        }

        if (node != undefined) {
            this.list.moveToFirst(node);
            var addBytes = item.itemBytes - node.v.itemBytes;
            if (addBytes > 0) {
               freeLruCache(this, addBytes, maxCacheBytes);
            }
            if (this.list.size() > 0){
              this.cachedBytes += addBytes;
              node.v = item;
              return;
            }
        }

        freeLruCache(this, item.itemBytes, maxCacheBytes);
        this.cachedData[cacheKey] = this.list.addFirst(item);
        this.cachedBytes += item.itemBytes;
    }

    LRUCache.prototype.remove = function(key) {
        var cacheKey = toCacheKey(key);
        var node = this.cachedData[cacheKey];
        if(node != undefined){
            this.cachedBytes -= node.v.itemBytes;
            this.list.remove(node);
            delete this.cachedData[cacheKey];
        }
    }

    LRUCache.prototype.clear = function() {
        this.cachedData = {};  
        this.list.clear();
        this.cachedBytes = 0;
    }

    LRUCache.prototype._assertValid = function() {
        var n = 0;
        var bytes = 0;
        for(var k in this.cachedData){
           n += 1;
           var node = this.cachedData[k];
           if (!node || !node.v || !node.v.key || node.v.key != k) {
             return false;
           }
           bytes += node.v.itemBytes;
        }
        return (n == this.list.size() && bytes == this.cachedBytes);
    }

    function freeLruCache(cache, requiredBytes, maxCacheBytes) {
        if (requiredBytes > maxCacheBytes) {
          throw new Error("bug: requiredBytes cannot be larger than cache max limit");
        }
        var cachedBytes = cache.cachedBytes;
        if (cachedBytes + requiredBytes <= maxCacheBytes) {
          return;
        }
        var list = cache.list;
        while (list.size() > 0 && maxCacheBytes - cachedBytes < requiredBytes) {
            var last = list.last().v;
            cachedBytes -= last.itemBytes;
            delete cache.cachedData[last.key];
            list.removeLast();
        }
        cache.cachedBytes = cachedBytes;
    }
    
    function isExpired(node, opts) {
        return (new Date().getTime() - node.v.refreshAt) / 60000 > opts.maxLivingMinutes;
    }

    function MBToBytes(MB){
        return MB * 1024 * 1024;
    }
    
    function toCacheKey(k) {
        return typeof (k) == "string" ? "s:" + k : "j:" + JSON.stringify(k);
    }

    var _hasWebStorage = (typeof (window.localStorage) != "undefined");
    var _disk = window.localStorage || {};
    function nothing() {
    }

    function toDiskCacheKey(key, user) {
        key = toCacheKey(key);
        var jsonVal = (typeof (key) != "string");
        if (user) {
            return (jsonVal ? "uj:" : "us:") + user + ":" + (jsonVal ? JSON.stringify(key) : key);
        }
        else {
            return (jsonVal ? "gj:" + JSON.stringify(key) : "gs:" + key);
        }
    }

    var _diskUser = diskGet(null, "user");

    function diskGet(key, user) {
        var val = _disk[toDiskCacheKey(key, user)];
        if (typeof (val) == "undefined") {
            return undefined;
        }
        var prefix = val.substring(0, 2);
        val = val.substring(2, val.length);
        return prefix == "j:" ? JSON.parse(val) : val;
    }

    function diskSet(key, val, user) {
        if (typeof (val) === "undefined") {
            return diskRemove(user, key);
        }
        if (typeof (val) == "string") {
            val = "s:" + val;
        }
        else {
            val = "j:" + JSON.stringify(val);
        }
        _disk[toDiskCacheKey(key, user)] = val;
        if (user && _diskUser != "" + user) {
            diskClear(user);
            diskSet("user", "" + user, null);
            _diskUser = "" + user;
        }
    }

    function diskRemove(key, user) {
        delete _disk[toDiskCacheKey(key, user)];
    }

    function diskClear(user) {
        console.log("diskClear, user:", user);
        var disk = _disk;
        if (!user) {
            disk.clear();
            return;
        }
        var s0 = "us:" + user + ":";
        var s1 = "uj:" + user + ":";
        for ( var s in _disk) {
            var begin = s.substring(0, s0.length);
            if (begin == s0 || begin == s1) {
                disk.removeItem(begin);
            }
        }
        // console.log("diskClear done");
    }

    var diskCache = {
        get : _hasWebStorage ? diskGet : nothing,
        set : _hasWebStorage ? diskSet : nothing,
        remove : _hasWebStorage ? diskRemove : nothing,
        clear : _hasWebStorage ? diskClear : nothing,
    };

    function compress(data) {
 	   var bytes = string2Bin(data);
       var deflate  = new z.Deflate(bytes, {
                            compressionType: z.Deflate.CompressionType.DYNAMIC
                          });
       var result = deflate.compress();
       return result;
    }

    function uncompress(data) {
 	   var inflate = new z.Inflate(data);
 	   var result = inflate.decompress();
       var b = bin2String(result);
       return b;
    }

    function string2Bin(str) {
        var length = str.length;
        var result = new Uint16Array(length);
        for (var i = 0; i < length; i++) {
          result[i] = str.charCodeAt(i);
        }
        return new Uint8Array(result.buffer);
    }

    var QUANTUM = 32768;
    function bin2String(array) {
       var result = '';
       var buffer = array.buffer;
       var length = array.byteLength/2;
       var offset = array.byteOffset;
       var stepLength = QUANTUM;
       while(length>0){
           if(length < QUANTUM){
               stepLength = length;
           }
           var temp = new Uint16Array(buffer, offset, stepLength);
           result += String.fromCharCode.apply(String, temp);
           offset += stepLength*2;
           length -= stepLength;              
       }           
      
       return result;
    }

    return {
        hasWebStorage : _hasWebStorage,
        LRUCache : LRUCache,
        disk : diskCache,
        compress : compress,
        uncompress : uncompress,
        lru  : new LRUCache(),
        clearAll : function() {
            $bip.log("clean all js cache content and web storage content.");
            this.lru = new LRUCache();
            this.disk.clear();
            for (tmpl in $.templates) {
                delete $.templates[tmpl];
            }
        },
    };
});
